Skip to content

add bundled provides guidance for srpm#92

Merged
oaubrey merged 1 commit into
RedHatProductSecurity:mainfrom
jasinner:srpm-bundled
May 28, 2026
Merged

add bundled provides guidance for srpm#92
oaubrey merged 1 commit into
RedHatProductSecurity:mainfrom
jasinner:srpm-bundled

Conversation

@jasinner

Copy link
Copy Markdown
Contributor

Summary

Document how to represent RPM bundled() / golang() Provides in build-time SBOMs, and update the RPM build example generator to emit them.

  • Add Bundled dependencies guidance to docs/sbom.md: collect Provides from Koji getRPMDeps across all build RPMs, model each bundled component as a separate package linked to the SRPM with DEPENDENCY_OF, and use typed purls per ecosystem.
  • Add sbom/examples/rpm/build/bundled_provides.py with parsing and SPDX/CycloneDX fragment helpers (mirrors Deptopia / manifest-cube logic).
  • Update from-koji.py to collect build Provides and append bundled packages to SPDX and CycloneDX output.
  • Update vim-9.1.083-5.el10 build and release SBOM examples with bundled(libvterm)pkg:generic/libvterm.

Design notes

  • Bundled provides appear on binary subpackage RPM headers (e.g. vim-enhanced provides bundled(libvterm)), not the SRPM. They are linked to the SRPM with DEPENDENCY_OF so one build SBOM deduplicates bundled deps across subpackages.
  • Generic bundled deps use pkg:generic/<name>; ecosystem-specific provides map to golang, pypi, npm, gem, cargo, or maven purl types.
  • Bundled components are distinct from SourceN archives (CONTAINS) and from components discovered by scanning an unpacked source tree.

Test plan

  • Review docs/sbom.md Bundled dependencies section for accuracy
  • Confirm vim-9.1.083-5.el10 SPDX/CycloneDX examples include SPDXRef-Bundled-11cdd6f19dc1 / pkg:generic/libvterm
  • Optional (requires Koji access): regenerate a build SBOM with ./from-koji.py brew vim-9.1.083-5.el10 and diff against committed examples

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for recording bundled/vendored dependencies as separate components in Software Bill of Materials (SBOM) documents, enabling more comprehensive tracking of vendored libraries in RPM packages.
  • Documentation

    • Added comprehensive guidance on capturing bundled dependencies from RPM specifications in SPDX and CycloneDX formats, including examples and linking specifications.

Walkthrough

This PR adds support for representing RPM spec-declared bundled/vendored components (declared via Provides: bundled(...) or Provides: golang(...)) as separate package objects in SPDX and CycloneDX SBOMs, with Koji integration, language-aware PURL generation, and deterministic relationship linking.

Changes

Bundled dependencies feature

Layer / File(s) Summary
Documentation of bundled dependencies concept
docs/sbom.md, sbom/examples/rpm/build/README.md
Introduces bundled dependencies concept: collection from Koji getRPMDeps (Provides type 1), creation as separate SPDX packages, linkage to SRPM via DEPENDENCY_OF, required fields (primaryPackagePurpose, downloadLocation, filesAnalyzed), typed PURL selection by language pattern, and distinction from source archives and scanned components.
Bundled provides parsing and format conversion
sbom/examples/rpm/build/bundled_provides.py
New module extracts bundled(...) / golang(...) provides from RPM metadata, maps inner names to (path, lang) using prefix/suffix heuristics for golang/python/nodejs/ruby/rust/java with generic fallback, deduplicates by (path, version, lang), and converts to SPDX package/relationship fragments and CycloneDX library components with language-derived PURLs and SHA-256–based deterministic SPDX IDs.
SBOM builder integration
sbom/examples/rpm/build/from-koji.py
Adds SBOMBuilder.get_build_provides(build_id) to collect Koji provides (type 1) per build RPM and SBOMBuilder.add_bundled_provides(build_id) to convert them to SPDX packages/relationships and CycloneDX components. Merges bundled components into main component list with version normalization, recomputes binary RPM provides, and restructures cdx["dependencies"] to include provides and conditional dependsOn references to bundled component bom-refs.
SBOM example outputs
sbom/examples/rpm/build/vim-*.json, sbom/examples/rpm/release/vim-*.json
Build and release vim SBOM examples updated to include libvterm as a generic bundled component with proper SPDX package entries (SPDXRef-Bundled-11cdd6f19dc1), PURL references (pkg:generic/libvterm), and DEPENDENCY_OF relationships linking bundled packages to SRPM.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding documentation and implementation for bundled RPM provides in SRPMs, which is the central focus across all modified files.
Description check ✅ Passed The description is directly related to the changeset, providing a summary of bundled provides documentation, code helpers, and example updates that match the file modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sbom/examples/rpm/build/bundled_provides.py`:
- Around line 42-45: The _bundled_purl function currently inserts dep.path
verbatim causing ambiguity for scoped npm names (e.g., `@scope/name`) —
percent-encode the path segment before building the purl (encode the leading @
as %40 and other reserved characters, while preserving path separators) by using
urllib.parse.quote with safe='/' (or equivalent) and then return
f"pkg:{purl_type}/{encoded_path}{ver}"; update the function (_bundled_purl) and
add the urllib.parse import (or your project's URL-encoding helper) so scoped
npm provides become pkg:npm/%40scope/name@...

In `@sbom/examples/rpm/build/from-koji.py`:
- Around line 598-601: Currently bundled_cdx_components are appended without
checking for existing bom-ref collisions, which can duplicate CycloneDX
component ids; before appending in the loop inside the routine that handles
bundled_cdx_components, compute an existing_bom_refs set from
self.cdx_components (use keys "bom-ref" and fallback to "purl"), then filter
bundled_cdx_components to skip any component whose "bom-ref" (from
_bundled_purl(b) / bundled_provides_to_cdx_components output) is already
present; keep the existing logic that pops version when None, but only append
components whose bom-ref is not in existing_bom_refs so create_cdx_from_spdx()
and the cdx["dependencies"][0]["dependsOn"] entries remain unambiguous.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 0bd410dd-94ba-4da2-96bc-0d0a10e76c5a

📥 Commits

Reviewing files that changed from the base of the PR and between 8148585 and 0952f75.

📒 Files selected for processing (8)
  • docs/sbom.md
  • sbom/examples/rpm/build/README.md
  • sbom/examples/rpm/build/bundled_provides.py
  • sbom/examples/rpm/build/from-koji.py
  • sbom/examples/rpm/build/vim-9.1.083-5.el10.cdx.json
  • sbom/examples/rpm/build/vim-9.1.083-5.el10.spdx.json
  • sbom/examples/rpm/release/vim-9.1.083-5.el10.cdx.json
  • sbom/examples/rpm/release/vim-9.1.083-5.el10.spdx.json

Comment on lines +42 to +45
def _bundled_purl(dep: BundledDep) -> str:
purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic")
ver = f"@{dep.version}" if dep.version else ""
return f"pkg:{purl_type}/{dep.path}{ver}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="sbom/examples/rpm/build/bundled_provides.py"
echo "== File: $FILE =="
sed -n '1,120p' "$FILE" | nl -ba | sed -n '1,120p'

echo
echo "== Lines around _bundled_purl (35-80) =="
sed -n '35,80p' "$FILE" | nl -ba

echo
echo "== Find BundledDep and dep.path construction =="
rg -n "class BundledDep|def _bundled_purl|dep\.path|_dep_lang_from_inner|LANG_TO_PURL_TYPE" "$FILE" -n

echo
echo "== Show full definitions related to bundling/parsing (where dep.path comes from) =="
# print likely relevant sections
sed -n '1,200p' "$FILE" | nl -ba | sed -n '1,200p'

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="sbom/examples/rpm/build/bundled_provides.py"

echo "== File head (first 160 lines) =="
sed -n '1,160p' "$FILE" | awk '{printf "%6d\t%s\n", NR, $0}'

echo
echo "== Lines around _bundled_purl (lines 30-90 approx) =="
sed -n '30,90p' "$FILE" | awk '{printf "%6d\t%s\n", NR+29, $0}'

echo
echo "== Grep key symbols =="
rg -n "class BundledDep|def _bundled_purl|dep\.path|_dep_lang_from_inner|LANG_TO_PURL_TYPE" "$FILE"

echo
echo "== Dump section that defines BundledDep and helpers (look 1-140) =="
sed -n '1,140p' "$FILE" | awk '{printf "%6d\t%s\n", NR, $0}'

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 13993


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "bundled\\(nodejs|golang\\(|bundled\\(|golang\\(" sbom/examples/rpm/build -S || true
rg -n "nodejs\\(|nodejs-\\w+|`@scope`|`@angular`|`@react`" sbom/examples/rpm/build -S || true
rg -n "bundled\\(" -S sbom/examples | head -n 200 || true
ls -R sbom/examples/rpm/build | head -n 200 || true

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 2281


🌐 Web query:

Package URL (purl) npm scoped package @scope ambiguity version delimiter percent-encoding guidance for namespace/name

💡 Result:

In the Package URL (purl) specification, to represent an npm scoped package (which typically uses the format @scope/name), the '@' character in the scope must be percent-encoded as %40 [1][2][3]. This is required to avoid ambiguity with the '@' character used as the version delimiter, which appears later in the purl syntax [2][3]. According to the standard purl syntax—pkg:npm//@—the version is separated from the package identifier by an unencoded '@' [4][2][3]. If the '@' symbol at the start of a scope were not encoded, parsers might incorrectly identify it as the start of the version string [3][5]. Therefore, a scoped package such as @storybook/addon is represented as pkg:npm/%40storybook/addon [1][3]. Key guidance for encoding namespaces and names in purl: - Scopes (namespaces): The leading '@' must be encoded as %40 [1][3]. - Separators: The '/' that separates the namespace (scope) from the name does not need to be and must not be percent-encoded [2][5]. - Version Delimiter: The '@' used to separate the package identifier from the version string is a structural separator and should not be encoded in that specific position [2][5]. - Consistency: The general rule for purl is that structural characters are only encoded if they appear outside their defined functional positions, where they would otherwise cause parsing ambiguity [2]. By using %40 for the scope's '@', the resulting PURL remains unambiguous and conformant with the specification [2][3].

Citations:


🌐 Web query:

SPDX externalRef purl encoding rules pkg:type/namespace/name@version percent-encoding reserved characters

💡 Result:

When using a Package URL (PURL) in an SPDX ExternalRef (referenceType: purl), the PURL must be encoded as an ASCII string using UTF-8 and percent-encoding as defined by RFC 3986 [1][2]. The following rules apply to percent-encoding within PURL components [1][2]: General Encoding Rules - All non-ASCII characters must be UTF-8 encoded and then percent-encoded [1][2]. - It is permissible to percent-encode any component, with the exception of the type [1][2]. - Producers and consumers must always percent-encode and percent-decode components and segments [1][2]. Separator and Reserved Character Rules - Type: Must not be encoded and must not contain separators [1][2]. - Separators (#,?, @,:) and delimiters (/, =) have specific handling: - The ':' (scheme/type separator) and '/' (type/namespace/name/subpath segment separator) must not be percent-encoded, as they are unambiguous [1][2]. - The '=' (qualifiers key/value separator) must not be encoded [1][2]. - The '@' (version separator), '?' (qualifiers separator), and '#' (subpath separator) must be percent-encoded as '%40', '%3F', and '%23' respectively if they appear elsewhere (i.e., when they are not acting as the primary separator for those components) [1][2]. When implementing an SPDX ExternalRef, the PURL is typically stored in the referenceLocator field [3][4]. Adhering to these encoding rules ensures compatibility across different SBOM producers and consumers [1][4].

Citations:


🌐 Web query:

CycloneDX component purl field percent encoding rules

💡 Result:

The Package URL (PURL) specification, which CycloneDX uses for component identification, mandates strict percent-encoding rules to ensure unambiguous parsing [1][2][3]. A PURL is defined as an ASCII string [2]. Key Percent-Encoding Rules: 1. General Requirement: Components and segments within a PURL must be percent-encoded according to RFC 3986 [2][3]. Non-ASCII characters must first be UTF-8 encoded and then percent-encoded [2]. 2. Separator Characters: Specific reserved characters acting as separators must NOT be percent-encoded when used in their designated structural roles [2][3]: - The colon ':' (scheme/type separator) must NOT be encoded [2][3]. - The forward slash '/' (used to separate type/namespace/name segments) must NOT be encoded [2][3]. - The '@' version separator, '?' qualifiers separator, and '#' subpath separator must NOT be encoded when functioning as delimiters [2][3]. - The '=' character used within qualifiers to separate keys and values must NOT be encoded [2]. 3. Encoding Reserved Characters Elsewhere: When reserved characters (such as '@', '?', '#', or ':') appear within a component's data (rather than as its primary separator), they must be percent-encoded to maintain the PURL's integrity [2]. For instance, '@' must be encoded as %40, '?' as %3F, and '#' as %23 when they occur inside a namespace, name, version, or qualifier value [2]. 4. The Type Component: The package 'type' MUST NOT be encoded [2][3]. It must be composed only of ASCII letters, numbers, periods ('.'), and dashes ('-'), and it must start with an ASCII letter [3]. 5. Producers and Consumers: All producers and consumers of PURL data are required to consistently apply these percent-encoding and decoding rules to ensure interoperability across different ecosystems and SBOM formats [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "bundled\\(nodejs|golang\\(|bundled\\(|golang\\(" sbom/examples/rpm/build -S || true
rg -n "nodejs\\(|nodejs-\\w+|`@scope`|`@angular`|`@react`" sbom/examples/rpm/build -S || true
rg -n "bundled\\(" -S sbom/examples | head -n 200 || true
ls -R sbom/examples/rpm/build | head -n 200 || true

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 2281


🌐 Web query:

Package URL (purl) npm scoped package @scope version delimiter percent-encoding guidance

💡 Result:

In the Package URL (purl) specification, scoped npm packages must be formatted by placing the scope in the namespace component and the package name in the name component [1][2]. Because the @ symbol is used as a delimiter for the version in a purl (e.g., name@version), the @ symbol at the beginning of an npm scope must be percent-encoded as %40 to avoid ambiguity and ensure correct parsing [3][4][5][6]. Guidance for construction: 1. Namespace: The scope (including the leading @) must be percent-encoded [1][7]. For example, the scope @angular becomes %40angular [5][8]. 2. Name: The package name itself should be placed in the name component [3][2]. 3. Version Delimiter: The @ symbol used to separate the package name from the version is a reserved separator and must not be percent-encoded in its role as a delimiter [1][9]. 4. Example: A scoped package named @angular/core with version 16.2.0 is represented as pkg:npm/%40angular/core@16.2.0 [8]. It is essential to follow this encoding rule because parsers use the @ character to identify the start of the version string; leaving the scope's @ unencoded can cause the parser to incorrectly identify the version or fail to parse the purl entirely [6]. All producers and consumers of purl data must adhere to these encoding rules to maintain interoperability across ecosystems [3][9].

Citations:


🌐 Web query:

Package URL purl encoding rules for pkg:type/namespace/name@version reserved characters percent-encoding

💡 Result:

The Package URL (purl) specification defines a standardized format to represent software package identity, using percent-encoding to ensure components can be parsed unambiguously [1][2][3]. A purl is strictly an ASCII string where non-ASCII characters must be UTF-8 encoded and then percent-encoded according to RFC 3986 [1][2]. The core encoding rules for purl components (namespace, name, version, qualifiers, and subpath) are as follows: 1. Mandatory Separators: Several characters are reserved as structural separators and must NOT be percent-encoded when used for their designated purpose [1][2]: - The colon ':' (after the 'pkg' scheme) [1][2]. - The forward slash '/' (separating type, namespace, name, and subpath segments) [1][2]. - The at sign '@' (version separator) [1][2]. - The question mark '?' (qualifiers separator) [1][2]. - The hash sign '#' (subpath separator) [1][2]. - The equal sign '=' (qualifiers key/value separator) [1][2]. 2. Encoding Reserved Characters: When these reserved characters appear within a component's data (rather than acting as a separator), they must be percent-encoded to prevent parsing ambiguity [1][4][2]. For example: - The '@' separator must be encoded as '%40' if it appears inside a component's value [1]. - The '?' separator must be encoded as '%3F' [1]. - The '#' separator must be encoded as '%23' [1]. - Any reserved character (e.g., '!', "'") that has no special meaning to the purl structure must still be percent-encoded [4]. 3. Type and Component-Specific Rules: - The package 'type' MUST NOT be percent-encoded and must not contain separators [1]. - Components like 'namespace', 'name', 'version', 'qualifiers', and 'subpath' must be percent-encoded [1][5][2]. - Producers and consumers must always perform percent-decoding when consuming data and percent-encoding when producing it [1]. - When percent-encoding is required, the string is first converted to UTF-8 bytes and then each byte is represented by a percent-encoded character triplet (e.g., '%XX') [1][2]. In summary, characters that do not function as designated separators must be percent-encoded if they are not in the 'unreserved' set (alphanumeric characters and '.-_~') [5][2][6]. Reserved characters acting as separators are left unencoded, while those same characters appearing as literal content within a component must be percent-encoded [1][4][2].

Citations:


🌐 Web query:

CycloneDX component purl percent encoding guidance

💡 Result:

CycloneDX guidance for percent-encoding in component purl/bom-ref is ultimately governed by the Package-URL (PURL) specification (ECMA-427), because CycloneDX’s purl field is a PURL string. 1) What percent-encoding must look like (PURL ECMA-427) - Percent-encoding is applied to specific PURL components (e.g., namespace, name, version, subpath) and is defined per component’s rules in ECMA-427: when percent-encoding is required, the component string is encoded as UTF-8 and then each “data octet” is replaced by a percent-encoded “character triplet” using RFC 3986 [1][2]. - In the percent-encoding rules, certain characters must not be percent-encoded (for example alphanumerics, punctuation, and separator characters when used as PURL separators), and the percent sign is not percent-encoded when it is already representing a percent-encoded character [1][2]. - Spaces must be percent-encoded as %20 [1][2]. 2) Canonical guidance on slashes (/) - In the PURL structure, the purl “namespace” and “name” components are separate segments; ECMA-427 specifies that namespace and name are “percent-encoded strings” (i.e., slashes that occur within those segment values are not “structural” separators and therefore can/should be encoded according to the component percent-encoding rules) [2]. 3) Practical CycloneDX ecosystem issue: encoded “/” (e.g., %2F) vs downstream tool expectations - CycloneDX tooling (example: cdxgen) has historically produced %2F in purl/bom-ref for Go modules, which caused interoperability friction with tools like Dependency-Track that couldn’t parse the URL-encoded purl as expected [3]. - A cdxgen issue describes “golang” PURLs containing URL-encoded module names (example: pkg:golang/github.com%2Fapache%2Fthrift@...), and notes that while this is technically defensible, it causes friction with downstream tools [4]. - CycloneDX community/tooling discussions include that this problem was fixed in a later cdxgen version (the reported fix was “Resolved in 10.3.0” for the Go module %2F encoding case) [4] and a separate issue notes a fix shipped in cdxgen version 8.3.3 for %2F appearing in purl/bom-ref [3]. 4) Bottom line guidance you can apply when generating CycloneDX purl strings - Do not “undo” percent-encoding inside purl just to make it look nicer (e.g., turning %2F back into /), because PURL’s grammar relies on correct percent-encoding for components that are treated as data rather than separators [1][2]. - If you are seeing %2F in the purl (common with Go module identifiers), treat it as a symptom of correctly encoding component data per ECMA-427, and handle downstream tool compatibility by using updated CycloneDX tooling or by ensuring your consumer/parsing tooling supports PURL percent-encoding (cdxgen explicitly discussed this interoperability issue and provided fixes in later versions) [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show how bundling README describes inputs
sed -n '1,120p' sbom/examples/rpm/build/README.md | awk '{printf "%5d\t%s\n", NR, $0}'

echo "== Search for npm purls in example outputs =="
rg -n "pkg:npm/" sbom/examples/rpm/build/*.cdx.json sbom/examples/rpm/build/*.spdx.json | head -n 100

echo
echo "== Search for raw '@' inside pkg:npm/ occurrences =="
# Match e.g. pkg:npm/@scope/name
rg -n "pkg:npm/[^[:space:]]*@[^[:space:]]*" sbom/examples/rpm/build/*.cdx.json sbom/examples/rpm/build/*.spdx.json || true

echo
echo "== Search for percent-encoded scope marker %40 in npm purls =="
rg -n "pkg:npm/%40" sbom/examples/rpm/build/*.cdx.json sbom/examples/rpm/build/*.spdx.json || true

echo
echo "== Inspect full purl strings around first match if any =="
python3 - <<'PY'
import glob, re
files = glob.glob("sbom/examples/rpm/build/*.cdx.json") + glob.glob("sbom/examples/rpm/build/*.spdx.json")
pat = re.compile(r'("referenceLocator"\s*:\s*")(?P<purl>pkg:[^"]+)(")')
for f in files:
    txt=open(f,'r',encoding='utf-8').read()
    m=None
    for m in pat.finditer(txt):
        p=m.group('purl')
        if p.startswith("pkg:npm/"):
            print("FILE", f)
            print(p)
            raise SystemExit
print("No pkg:npm purls found")
PY

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 2695


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for npm purls in example outputs (non-fatal if none) =="
rg -n "pkg:npm/" sbom/examples/rpm/build/*.cdx.json sbom/examples/rpm/build/*.spdx.json || true

echo
echo "== Search for pkg:npm/ then any '@' after =="
rg -n "pkg:npm/[^[:space:]]*@[^[:space:]]*" sbom/examples/rpm/build/*.cdx.json sbom/examples/rpm/build/*.spdx.json || true

echo
echo "== Search for percent-encoded npm scope marker %40 in npm purls =="
rg -n "pkg:npm/%40" sbom/examples/rpm/build/*.cdx.json sbom/examples/rpm/build/*.spdx.json || true

echo
echo "== Search for bundled(nodejs( pattern in source/docs =="
rg -n "bundled\\(nodejs\\(|golang\\(nodejs\\(" -S sbom/examples/rpm/build sbom/examples | head -n 200 || true

echo
echo "== Search for '@' inside dep.path construction inputs (nodejs- / npm) in this file =="
rg -n "nodejs-|npm\\b|nodejs\\b" sbom/examples/rpm/build/bundled_provides.py

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 612


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import glob, re, json

files = glob.glob("sbom/examples/rpm/build/*.cdx.json") + glob.glob("sbom/examples/rpm/build/*.spdx.json")
pat = re.compile(r'^pkg:npm/.*$', re.M)

found=[]
for f in files:
    txt=open(f,'r',encoding='utf-8').read()
    for m in re.finditer(r'"referenceLocator"\s*:\s*"([^"]+)"', txt):
        v=m.group(1)
        if v.startswith("pkg:npm/"):
            found.append((f,v))
            break

print("npm purl count:", len(found))
for f,v in found[:50]:
    print(f, "=>", v)
PY

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="sbom/examples/rpm/build/bundled_provides.py"
wc -l "$FILE"

echo "== Tail (last 160 lines) =="
tail -n 160 "$FILE" | awk '{printf "%6d\t%s\n", NR+('"$(wc -l <"$FILE")'-160), $0}' || true

echo
echo "== Show remaining portion starting at 120 =="
sed -n '120,260p' "$FILE" | awk '{printf "%6d\t%s\n", NR+119, $0}'

echo
echo "== Show remaining portion starting at 260 =="
sed -n '260,420p' "$FILE" | awk '{printf "%6d\t%s\n", NR+259, $0}'

echo
echo "== Find all _bundled_purl call sites in this file =="
rg -n "_bundled_purl\\(" "$FILE"

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="sbom/examples/rpm/build/bundled_provides.py"
wc -l "$FILE"

echo "== Tail 80 lines =="
tail -n 80 "$FILE" | sed -n '1,80p' | awk '{printf "%6d\t%s\n", NR, $0}'

echo
echo "== Show ranges 120-189 =="
sed -n '120,189p' "$FILE" | awk '{printf "%6d\t%s\n", NR+119, $0}'

echo
echo "== Call sites for _bundled_purl =="
rg -n "_bundled_purl\\(" "$FILE"

echo
echo "== CycloneDX conversion section (look for cyclonedx/lang) =="
rg -n "cyclonedx|bom-ref|component.*purl|referenceLocator|purl" "$FILE"

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 6569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "bundled\\(nodejs\\(" -S . || true
rg -n "bundled\\(nodejs\\(.*@.*\\/" -S . || true
rg -n "@[a-zA-Z0-9._-]+/" sbom -S || true
rg -n "nodejs\\([^)]*@[^)]*\\)" -S . || true
rg -n "npm scoped|scoped npm|%40|pkg:npm/" -S sbom docs . || true

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 546


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "pkg:npm/%40" docs sbom -S || true
rg -n "bundled\\(nodejs\\([^)]*@[^)]*\\)" docs/sbom.md -S || true
rg -n "bundled\\(nodejs\\(.*@.*\\/.*\\)\\)" docs/sbom.md -S || true
rg -n "@[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+" docs/sbom.md -S || true
rg -n "nodejs\\-@|bundled\\(nodejs\\(\\@|%40" docs/sbom.md -S || true

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "pkg:npm/%40" docs sbom -S || true
rg -n "bundled\\(nodejs\\([^)]*@[^)]*\\)" docs/sbom.md -S || true
rg -n "bundled\\(nodejs\\(.*@.*\\/.*\\)\\)" docs/sbom.md -S || true
rg -n "@[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+" docs/sbom.md -S || true
rg -n "nodejs\\-@|bundled\\(nodejs\\(\\@|%40" docs/sbom.md -S || true

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

DOC="docs/sbom.md"

echo "== Search for %40 in docs/sbom.md =="
rg -n "%40" "$DOC" || true

echo
echo "== Search for common scoped-npm patterns in docs/sbom.md =="
rg -n "@[a-zA-Z0-9_.-]+/" "$DOC" || true
rg -n "scoped npm|npm scoped|%40|pkg:npm/" "$DOC" || true

echo
echo "== Show bundled dependencies section around line 630-700 =="
sed -n '620,690p' "$DOC" | awk '{printf "%5d\t%s\n", NR+619, $0}'

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 3502


Percent-encode bundled purl paths (npm scoped @... ambiguity)

_bundled_purl injects dep.path verbatim into pkg:{purl_type}/{dep.path}@{version}. For scoped npm provides like bundled(nodejs(@scope/name)), this yields pkg:npm/@scope/name@..., but npm scope @ must be percent-encoded (e.g., %40scope) to avoid ambiguity with the purl version delimiter.

Suggested fix
 import hashlib
 from dataclasses import dataclass
 from typing import Any
+from urllib.parse import quote
@@
 def _bundled_purl(dep: BundledDep) -> str:
     purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic")
     ver = f"@{dep.version}" if dep.version else ""
-    return f"pkg:{purl_type}/{dep.path}{ver}"
+    encoded_path = "/".join(quote(part, safe="") for part in dep.path.split("/"))
+    return f"pkg:{purl_type}/{encoded_path}{ver}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _bundled_purl(dep: BundledDep) -> str:
purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic")
ver = f"@{dep.version}" if dep.version else ""
return f"pkg:{purl_type}/{dep.path}{ver}"
def _bundled_purl(dep: BundledDep) -> str:
purl_type = LANG_TO_PURL_TYPE.get(dep.lang, "generic")
ver = f"@{dep.version}" if dep.version else ""
encoded_path = "/".join(quote(part, safe="") for part in dep.path.split("/"))
return f"pkg:{purl_type}/{encoded_path}{ver}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sbom/examples/rpm/build/bundled_provides.py` around lines 42 - 45, The
_bundled_purl function currently inserts dep.path verbatim causing ambiguity for
scoped npm names (e.g., `@scope/name`) — percent-encode the path segment before
building the purl (encode the leading @ as %40 and other reserved characters,
while preserving path separators) by using urllib.parse.quote with safe='/' (or
equivalent) and then return f"pkg:{purl_type}/{encoded_path}{ver}"; update the
function (_bundled_purl) and add the urllib.parse import (or your project's
URL-encoding helper) so scoped npm provides become pkg:npm/%40scope/name@...

Comment on lines +598 to +601
for bundled_component in bundled_cdx_components:
if bundled_component.get("version") is None:
bundled_component.pop("version", None)
self.cdx_components.append(bundled_component)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant section around the reported line numbers
FILE="sbom/examples/rpm/build/from-koji.py"
nl -ba "$FILE" | sed -n '560,680p'

# Find where bom-ref is set/normalized in this file
rg -n --hidden --no-ignore-vcs "bom-ref|bom_ref|dependsOn|cdx\\[\"dependencies\"\\]" "$FILE"

# Print helper functions / blocks that build component identity or bom-ref
rg -n --hidden --no-ignore-vcs "purl|bom.*ref|component.*ref|cyclonedx" "$FILE"

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm file exists
ls -la "sbom/examples/rpm/build/from-koji.py"

# Show lines around 580-630 with line numbers (using awk since nl isn't available)
awk 'NR>=560 && NR<=650 {printf "%d\t%s\n", NR, $0}' "sbom/examples/rpm/build/from-koji.py"

# Search for bom-ref / dependency wiring
rg -n --hidden --no-ignore-vcs "bom-ref|bom_ref|dependsOn|cdx\\[\"dependencies\"\\]" "sbom/examples/rpm/build/from-koji.py"

# Search for where bom-ref/purl/component identity is constructed
rg -n --hidden --no-ignore-vcs "purl|bom.*ref|component.*ref|cyclonedx|cdx_components" "sbom/examples/rpm/build/from-koji.py"

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 6698


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="sbom/examples/rpm/build/from-koji.py"

# Show function where bundled provides are converted to CycloneDX components
rg -n "def add_bundled_provides|bundled_provides_to_cdx_components" "$FILE"
# Show the relevant implementations (around the matches)
awk 'NR>=1 && NR<=320 {printf "%d\t%s\n", NR, $0}' "$FILE" | head -n 320
awk 'NR>=320 && NR<=560 {printf "%d\t%s\n", NR, $0}' "$FILE" | head -n 240
awk 'NR>=520 && NR<=640 {printf "%d\t%s\n", NR, $0}' "$FILE" | head -n 200

# Also locate how bom-ref is constructed for syft/spdx components
rg -n "create_cdx_from_spdx\\(|syft_cdx_components|self\\.cdx_components\\.extend\\(syft_cdx_components|pkg:generic/|pkg:rpm/" "$FILE"

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 29053


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="sbom/examples/rpm/build/from-koji.py"

# Show create_cdx_from_spdx implementation
awk 'NR>=640 && NR<=740 {printf "%d\t%s\n", NR, $0}' "$FILE"

# Locate bundled_provides.py and inspect relevant function(s)
ls -la sbom/examples/rpm/build
fd -a "bundled_provides.py" -t f . | head -n 20

# If found, print around bundled_provides_to_cdx_components and bom-ref construction
BFILE="$(fd -a "bundled_provides.py" -t f sbom/examples/rpm/build sbom/examples/rpm -x echo 2>/dev/null | head -n 1 || true)"
if [ -z "${BFILE}" ]; then
  BFILE="$(fd -a "bundled_provides.py" -t f . | head -n 1)"
fi
echo "Using bundled providers file: $BFILE"

rg -n "def bundled_provides_to_cdx_components|bom-ref|bom_ref|dependsOn|purl" "$BFILE"
awk 'NR>=1 && NR<=260 {printf "%d\t%s\n", NR, $0}' "$BFILE" | head -n 260

# Print just the function body region more directly
rg -n "def bundled_provides_to_cdx_components" "$BFILE" | head -n 5
start=$(rg -n "def bundled_provides_to_cdx_components" "$BFILE" | head -n 1 | cut -d: -f1)
if [ -n "${start}" ]; then
  awk -v s="$start" 'NR>=s && NR<=s+220 {printf "%d\t%s\n", NR, $0}' "$BFILE"
fi

Repository: RedHatProductSecurity/security-data-guidelines

Length of output: 11693


Deduplicate bundled CycloneDX components by bom-ref before appending them.

  • bundled_provides_to_cdx_components() emits "bom-ref"/"purl" from _bundled_purl(b), and create_cdx_from_spdx() also uses the package-manager purl as "bom-ref". Since syft components are appended without any "bom-ref" dedupe, a bundled provide can collide with an existing syft/spdx component and produce duplicate CycloneDX component identifiers.
  • cdx["dependencies"][0]["dependsOn"] (lines 615-618) is then built from bundled "bom-ref" values, so duplicate ids make dependency references ambiguous/invalid for BOM consumers.
Suggested fix
         copy_of_cdx_root["pedigree"] = {"ancestors": cdx_pedigrees}
         self.cdx_components.append(copy_of_cdx_root)
+        existing_bom_refs = {component["bom-ref"] for component in self.cdx_components}
         for bundled_component in bundled_cdx_components:
             if bundled_component.get("version") is None:
                 bundled_component.pop("version", None)
-            self.cdx_components.append(bundled_component)
+            if bundled_component["bom-ref"] not in existing_bom_refs:
+                self.cdx_components.append(bundled_component)
+                existing_bom_refs.add(bundled_component["bom-ref"])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for bundled_component in bundled_cdx_components:
if bundled_component.get("version") is None:
bundled_component.pop("version", None)
self.cdx_components.append(bundled_component)
copy_of_cdx_root["pedigree"] = {"ancestors": cdx_pedigrees}
self.cdx_components.append(copy_of_cdx_root)
existing_bom_refs = {component["bom-ref"] for component in self.cdx_components}
for bundled_component in bundled_cdx_components:
if bundled_component.get("version") is None:
bundled_component.pop("version", None)
if bundled_component["bom-ref"] not in existing_bom_refs:
self.cdx_components.append(bundled_component)
existing_bom_refs.add(bundled_component["bom-ref"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sbom/examples/rpm/build/from-koji.py` around lines 598 - 601, Currently
bundled_cdx_components are appended without checking for existing bom-ref
collisions, which can duplicate CycloneDX component ids; before appending in the
loop inside the routine that handles bundled_cdx_components, compute an
existing_bom_refs set from self.cdx_components (use keys "bom-ref" and fallback
to "purl"), then filter bundled_cdx_components to skip any component whose
"bom-ref" (from _bundled_purl(b) / bundled_provides_to_cdx_components output) is
already present; keep the existing logic that pops version when None, but only
append components whose bom-ref is not in existing_bom_refs so
create_cdx_from_spdx() and the cdx["dependencies"][0]["dependsOn"] entries
remain unambiguous.

@oaubrey
oaubrey merged commit b811911 into RedHatProductSecurity:main May 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants